package com.nulldev.util.web.HttpServer3;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.WritableByteChannel;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;

import com.nulldev.util.JVM.Arguments;
import com.nulldev.util.VariableAPI.MathUtil;
import com.nulldev.util.concurrency.threadIt.v4.emapi.cntft.ControllableFutureTask;
import com.nulldev.util.data.Variables;
import com.nulldev.util.data.Arrays.maps.FastMaps;
import com.nulldev.util.data.QueuedArrays.QueuedMap;
import com.nulldev.util.io.IOUtils;
import com.nulldev.util.io.extensions.copyable.Copyable;
import com.nulldev.util.io.extensions.copyable.nio.NIOCopyable;
import com.nulldev.util.io.file.FileUtil;
import com.nulldev.util.web.HttpServer3.legacyPages.v1.HttpPage;
import com.nulldev.util.web.HttpServer3.legacyPages.v1.RequestType;
import com.nulldev.util.web.HttpServer3.legacyPages.v2.HttpPage2;
import com.nulldev.util.web.HttpServer3.pages.v3.ASyncHttpPage3;
import com.nulldev.util.web.HttpServer3.pages.v3.HttpPage3;
import com.nulldev.util.web.HttpServer3.pages.v3.HttpPage3.Request;
import com.nulldev.util.web.HttpServer3.util.BlankHashMap;

public class MicroCache {

	private final long limit;
	private final static boolean ENABLE_QUEUED_MAPS = Arguments.hasArgument("--MicroCache.useQueuedMaps");
	private final Map<String, CachedPage> cache = ENABLE_QUEUED_MAPS ? QueuedMap.<String, MicroCache.CachedPage>make(48) : FastMaps.map(48);
	public static final int DEFAULT_SIZE = 8 * 1024 * 1024;

	private final AtomicLong size = new AtomicLong();

	public MicroCache() {
		this(DEFAULT_SIZE); // 8MB
	}

	public MicroCache(final long limit) {
		if (limit < 1)
			throw new IllegalArgumentException("limit < 1!");
		this.limit = limit;
	}

	public CachedPage cache(final String name, final Page page) {
		if (page == null)
			throw new NullPointerException("page == null!");
		final CachedPage c = new CachedPage(name, page);
		if (this.size.longValue() + c.size() < this.limit) {
			this.size.getAndAdd(c.size());
			this.cache.put(name, c);
			return c;
		} else {
			c.close();
			return null;
		}
	}

	public long getLimit() {
		return this.limit;
	}

	public long getSize() {
		return this.size.get();
	}

	public boolean isFull() {
		return this.size.get() >= this.limit;
	}

	@Override
	public String toString() {
		final float usage = ((float) this.size.get() / (float) this.limit) * 100;
		return "Microcache[hash=" + MathUtil.toHex(this.hashCode()) + ",size=" + FileUtil.convertToStringRepresentation(this.limit) + ",usage="
				+ FileUtil.convertToStringRepresentation(this.size.get()) + "(" + String.format("%.2f", usage) + "%)]";
	}

	private static final BlankHashMap<String, String> STUB_HEADERS = new BlankHashMap<String, String>();

	public static class CachedPage implements Page, Copyable, NIOCopyable {
		private final Page page;
		private transient byte[] cachedResponse;
		private final String id;

		public CachedPage(final String id, final Page page) {
			this.page = Variables.requireNonNullObject(page, "page");
			this.id = Variables.requireNonNullObject(id, "id");
			if (page instanceof ASyncHttpPage3) {
				try {
					final ControllableFutureTask<Response> _r = new ControllableFutureTask<Response>();
					((ASyncHttpPage3) this.page).onFutureRequest(new Request("/cache?caching=true", "0.0.0.0"), _r);
					final Response r = _r.waitTillCompletion();
					if (r.copyable()) {
						final ByteArrayOutputStream baos = new ByteArrayOutputStream(IOUtils.MEMORY_ALLOC_BUFFER);
						r.getCopyable().writeTo(baos);
						this.cachedResponse = baos.toByteArray();
						IOUtils.closeQuietly(baos);
					} else {
						this.cachedResponse = IOUtils.toByteArray(r.getInputStream());
					}
				} catch (Exception e) {
					throw new RuntimeException(e);
				}
			} else if (page instanceof HttpPage3) {
				try {
					final Response resp = ((HttpPage3) this.page).onRequest(new Request("/cache?caching=true", "0.0.0.0"));
					if (resp.copyable()) {
						final ByteArrayOutputStream baos = new ByteArrayOutputStream(IOUtils.MEMORY_ALLOC_BUFFER);
						resp.getCopyable().writeTo(baos);
						this.cachedResponse = baos.toByteArray();
						IOUtils.closeQuietly(baos);
					} else {
						this.cachedResponse = IOUtils.toByteArray(resp.getInputStream());
					}
				} catch (IOException e) {
					throw new RuntimeException(e);
				}
			} else if (page instanceof HttpPage2) {
				try {
					this.cachedResponse = IOUtils
							.toByteArray(((HttpPage2) this.page).request(RequestType.GET, "/cache?caching=true", STUB_HEADERS, null, "0.0.0.0").getB());
				} catch (IOException e) {
					throw new RuntimeException(e);
				}
			} else if (page instanceof HttpPage) {
				try {
					final HttpPage p = ((HttpPage) this.page);
					if (p.sendRaw()) {
						this.cachedResponse = IOUtils.toByteArray(new FileInputStream(p.getPath()));
					} else if (p.isOverBytes()) {
						this.cachedResponse = p.handleRequest_Alt("/cache?caching=true", "0.0.0.0", RequestType.GET);
					} else {
						this.cachedResponse = p.handleRequest("/cache?caching=true", "0.0.0.0", RequestType.GET).getBytes();
					}
				} catch (Exception e) {
					throw new RuntimeException(e);
				}
			} else {
				throw new UnsupportedOperationException("Unsupported object: " + page);
			}
		}

		public byte[] get() {
			return this.cachedResponse;
		}

		public int size() {
			return this.cachedResponse.length;
		}

		public Page page() {
			return this.page;
		}

		public void close() {
			this.cachedResponse = null;
		}

		public String getID() {
			return this.id;
		}

		public InputStream getAsStream() {
			return new ByteArrayInputStream(this.cachedResponse, 0, this.cachedResponse.length);
		}

		@Override
		public void writeTo(final OutputStream os) throws IOException {
			os.write(this.cachedResponse, 0, this.cachedResponse.length);
		}

		@Override
		public void writeTo(final WritableByteChannel os) throws IOException {
			os.write(ByteBuffer.wrap(this.cachedResponse, 0, this.cachedResponse.length));
		}
	}
}
